package yolo

import (
	"appengine"
	"appengine/datastore"
	"appengine/memcache"
	"appengine/taskqueue"
	"encoding/json"
	"fmt"
	"net/http"
	"reflect"
	"strings"
	"time"
)

func init() {
	// Flush the memcache to the datastore.
	http.HandleFunc("/worker/cache_flush", handleCacheFlush)
}

var types = make(map[string]reflect.Type)

func RegisterCacheableType(v interface{}) {
	rt := reflect.TypeOf(v)
	if reflect.ValueOf(v).Kind() == reflect.Ptr {
		rt = reflect.ValueOf(v).Elem().Type()
	}
	types[rt.String()] = rt
}

type Write struct {
	Key string
	Type string
}

type PendingWrites []Write

func handleCacheFlush(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	keys := strings.Split(r.FormValue("key"), ",")
	typeNames := strings.Split(r.FormValue("type"), ",")

	if len(keys) != len(typeNames) {
		panic("length of keys and typenames mismatch")
	}

	if len(keys) == 0 {
		c.Warningf("%s: No key specified", currentLocation())
		return
	}

	vs := []interface{}{}
	for i, typeName := range typeNames {
		rt, ok := types[typeName]
		if !ok {
			c.Errorf("Cache: Unknown type %s queried", typeName)
			return
		}
		v := reflect.New(rt).Interface()
		err := GetCachedObject(c, keys[i], v)
		if err != nil {
			c.Warningf("Cache: get object failed: %s for: %s (%t)", err.Error(), keys[i], v)
			return
		}
		vs = append(vs, v)
	}

	err := FlushCachedObjectMulti(c, keys, vs)
	if err != nil {
		c.Warningf("Cache: flush to datastore failed: %s: %s", r.FormValue("key"), err.Error())
		return
	}
	c.Infof("**** Cache: written %d objects to datastore (%s)", len(keys), typeNames[0])
}

func DeleteCachedObject(c appengine.Context, key string) error {
	getDirtyCounter(c, key).Delete(c)
	memcache.Delete(c, key)
	datastoreKey, err := datastore.DecodeKey(key)
	if err != nil {
		c.Errorf("Cache: Invalid cache key %s: %s", key, err.Error())
		return err
	}
	return datastore.Delete(c, datastoreKey)
}

func GetCachedObject(c appengine.Context, key string, v interface{}) error {
	err := GetCachedObjectInMemory(c, key, v)
	if err == nil {
		return nil
	}
	datastoreKey, err := datastore.DecodeKey(key)
	if err != nil {
		return err
	}
	err = datastore.Get(c, datastoreKey, v)
	if err == nil {
		bs, err := json.Marshal(v)
		if err != nil {
			return err
		}
		err = memcache.Set(c, &memcache.Item{Key: key, Value: bs})
	}
	return err
}

func GetCachedObjectInMemory(c appengine.Context, key string, v interface{}) error {
	if key == "" {
		return NewYoloError("Null key is passed.")
	}
	_, err := memcache.JSON.Get(c, key, v)
	return err
}

func GetOrCreateCachedObject(c appengine.Context, key string, v interface{}, created *bool) error {
	if key == "" {
		return NewYoloError("Null key is passed.")
	}
	_, err := memcache.JSON.Get(c, key, v)
	if err == nil {
		return nil
	}
	datastoreKey, err := datastore.DecodeKey(key)
	if err != nil {
		return err
	}
	fn := func(c appengine.Context) error {
		err = datastore.Get(c, datastoreKey, v)
		if err == datastore.ErrNoSuchEntity {
			_, err = datastore.Put(c, datastoreKey, v)
			if err != nil && created != nil {
				*created = true
			}
		}
		if err == nil {
			bs, err := json.Marshal(v)
			if err != nil {
				return err
			}
			err = memcache.Set(c, &memcache.Item{Key: key, Value: bs})
		}
		return err
	}
	return datastore.RunInTransaction(c, fn, nil)
}

func StoreCachedObject(c appengine.Context, key string, v interface{}, immediate bool) error {
	err := StoreCachedObjectInMemory(c, key, v)
	if err != nil {
		return err
	}
	if immediate {
		return FlushCachedObject(c, key, v)
	}
	PostDelayedCacheFlush(c, key, v)
	return nil
}

func StoreCachedObjectInMemory(c appengine.Context, key string, v interface{}) error {
	bs, err := json.Marshal(v)
	if err != nil {
		return err
	}
	return memcache.Set(c, &memcache.Item{Key: key, Value: bs})
}

func UpdateCachedObject(c appengine.Context, key string, v interface{}, updater func(interface{}) error, storeImmediate bool) error {
	err := UpdateCachedObjectInMemory(c, key, v, updater)
	if err != nil {
		return err
	}
	if storeImmediate {
		return FlushCachedObject(c, key, v)
	}
	PostDelayedCacheFlush(c, key, v)
	return nil
}

func UpdateCachedObjectInMemory(c appengine.Context, key string, v interface{}, updater func(interface{}) error) error {
	// Make sure we have the item in the cache.
	err := GetCachedObject(c, key, v)
	if err != nil {
		return err
	}
	return updateMemcacheItem(c, key, v, updater)
}

func FlushCachedObject(c appengine.Context, key string, v interface{}) error {
	return FlushCachedObjectMulti(c, []string{key}, []interface{}{v})
}

func FlushCachedObjectMulti(c appengine.Context, stringKeys []string, v []interface{}) error {
	keys := []*datastore.Key{}
	for _, key := range stringKeys {
		datastoreKey, err := datastore.DecodeKey(key)
		if err != nil {
			c.Errorf("Cache: Invalid cache key: %s", key)
			return err
		}
		keys = append(keys, datastoreKey)
		getDirtyCounter(c, key).Delete(c)
	}
	_, err := datastore.PutMulti(c, keys, v)
	if err != nil {
		c.Errorf("Cache: %s writing to datastore failed: %s", strings.Join(stringKeys, ", "), err.Error())
		return err
	}
	return nil
}

func PostDelayedCacheFlush(c appengine.Context, key string, v interface{}) error {
	return PostDelayedCacheFlushMulti(c, []string{key}, []interface{}{v})
}

func PostDelayedCacheFlushMulti(c appengine.Context, keys []string, v []interface{}) error {
	if len(keys) == 0 {
		c.Warningf("%s: no keys to flush", currentLocation())
		return nil
	}
	types := []string{}
	for i, key := range keys {
		dirty, _ := getDirtyCounter(c, key).Increment(c)
		if dirty >= 20 {
			// If we have many dirty writes write them out now.
			return FlushCachedObjectMulti(c, keys, v)
		}

		rt := reflect.TypeOf(v[i])
		if reflect.ValueOf(v[i]).Kind() == reflect.Ptr {
			rt = reflect.ValueOf(v[i]).Elem().Type()
		}
		types = append(types, rt.String())
	}
	t := taskqueue.NewPOSTTask("/worker/cache_flush",
		map[string][]string{
			"key":{strings.Join(keys, ",")},
			"type":{strings.Join(types, ",")}})
	t.Delay = 5 * 1000 * 1000
	tm := time.Now().Unix() &^ 3
	t.Name = fmt.Sprintf("CacheDelayedFlush-%s-%v", keys[0], tm)
	_, err := taskqueue.Add(c, t, "cache")
	if err != nil {
		// c.Debugf("POST ERROR: %s", t.Name)
	}
	return nil
}

func getDirtyCounter(c appengine.Context, key string) *Counter {
	return &Counter{fmt.Sprintf("Cache-%s", key)}
}
